package com.russell.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Date Utility Class. This is used to convert Strings to Dates and Timestamps.
 * Utility also contains common date methods.
 * 
 * 
 * 
 * 
 * @author <a href="mailto:fvinluan@gmail.com">Francis Vinluan</a>
 */
public final class DateUtil {
	private final static Logger logger = LoggerFactory
			.getLogger(DateUtil.class);

	/**
	 * Adds to a date returning a new object. The original date object is
	 * unchanged.
	 * 
	 * @param date
	 *            the date, not null
	 * @param calendarField
	 *            the calendar field to add to
	 * @param amount
	 *            the amount to add, may be negative
	 * @return the new date object with the amount added
	 * @throws IllegalArgumentException
	 *             if the date is null
	 * 
	 */
	private static Date add(Date date, int calendarField, int amount) {
		if (date == null) {
			throw new IllegalArgumentException("The date must not be null");
		}
		Calendar c = Calendar.getInstance();
		c.setTime(date);
		c.add(calendarField, amount);
		return c.getTime();
	}

	/**
	 * Adds a number of days to a date returning a new object. The original date
	 * object is unchanged.
	 * 
	 * @param date
	 *            the date, not null
	 * @param amount
	 *            the amount to add, may be negative
	 * @return the new date object with the amount added
	 * @throws IllegalArgumentException
	 *             if the date is null
	 */
	public static Date addDays(Date date, int amount) {
		return add(date, Calendar.DAY_OF_MONTH, amount);
	}

	/**
	 * Adds a number of hours to a date returning a new object. The original
	 * date object is unchanged.
	 * 
	 * @param date
	 *            the date, not null
	 * @param amount
	 *            the amount to add, may be negative
	 * @return the new date object with the amount added
	 * @throws IllegalArgumentException
	 *             if the date is null
	 */
	public static Date addHours(Date date, int amount) {
		return add(date, Calendar.HOUR_OF_DAY, amount);
	}

	/**
	 * Adds a number of months to a date returning a new object. The original
	 * date object is unchanged.
	 * 
	 * @param date
	 *            the date, not null
	 * @param amount
	 *            the amount to add, may be negative
	 * @return the new date object with the amount added
	 * @throws IllegalArgumentException
	 *             if the date is null
	 */
	public static Date addMonths(Date date, int amount) {
		return add(date, Calendar.MONTH, amount);
	}

	/**
	 * Adds a number of seconds to a date returning a new object. The original
	 * date object is unchanged.
	 * 
	 * @param date
	 *            the date, not null
	 * @param amount
	 *            the amount to add, may be negative
	 * @return the new date object with the amount added
	 * @throws IllegalArgumentException
	 *             if the date is null
	 */
	public static Date addSeconds(Date date, int amount) {
		return add(date, Calendar.SECOND, amount);
	}

	/**
	 * Adds a number of weeks to a date returning a new object. The original
	 * date object is unchanged.
	 * 
	 * @param date
	 *            the date, not null
	 * @param amount
	 *            the amount to add, may be negative
	 * @return the new date object with the amount added
	 * @throws IllegalArgumentException
	 *             if the date is null
	 */
	public static Date addWeeks(Date date, int amount) {
		return add(date, Calendar.WEEK_OF_YEAR, amount);
	}

	/**
	 * Returns the Date equivalent of the GMT Date Time parameter.
	 * 
	 * @param strDate
	 *            String representation of the Date being processed. This should
	 * @param aMask
	 *            String format of the date
	 * @return
	 * @throws ParseException
	 */
	public static Date convertGMTDateStringToDate(String strDate, String aMask)
			throws ParseException {

		SimpleDateFormat format = new SimpleDateFormat(aMask);
		format.setTimeZone(TimeZone.getTimeZone("GMT"));
		return format.parse(strDate);
	}

	/**
	 * This method generates a string representation of a date/time in the
	 * format you specify on input.
	 * 
	 * @param dateString
	 * @param pattern
	 * @return
	 * @throws ParseException
	 */
	public static final Date convertStringToDate(String dateString,
			String pattern) throws ParseException {
		if (StringUtils.isBlank(dateString)) {
			return null;
		}

		Date date = null;
		DateFormat df = new SimpleDateFormat(pattern);

		if (logger.isDebugEnabled()) {
			logger.debug("converting '" + dateString + "' to date with mask '"
					+ pattern + "'");
		}

		try {
			date = df.parse(dateString);
		} catch (ParseException pe) {
			// logger.error("ParseException: " + pe);
			throw new ParseException(pe.getMessage(), pe.getErrorOffset());
		}

		return (date);
	}

	/**
	 * Returns a new instance of the date passed. Mainly used to make sure that
	 * date will not mutate
	 * 
	 * @param date
	 *            date which the new date will be based on
	 * @return a converted Date object
	 */
	public static Date copyDate(Date date) {

		if (date == null) {
			return null;
		}

		return new Date(date.getTime());
	}

	/**
	 * Calculates the difference in days of the given dates.
	 * 
	 * @param startDate
	 * @param endDate
	 * @return int
	 */
	public static long dayDifference(Date startDate, Date endDate) {

		Date tmpStartDate = DateUtils
				.truncate(startDate, Calendar.DAY_OF_MONTH);
		Date tmpEndDate = DateUtils.truncate(endDate, Calendar.DAY_OF_MONTH);

		GregorianCalendar startCalendar = new GregorianCalendar();
		startCalendar.setTime(tmpStartDate);

		GregorianCalendar endCalendar = new GregorianCalendar();
		endCalendar.setTime(tmpEndDate);

		long milliseconds1 = startCalendar.getTimeInMillis();
		long milliseconds2 = endCalendar.getTimeInMillis();
		long diff = milliseconds2 - milliseconds1;
		long diffMinutes = diff / (1000 * 60 * 60 * 24);

		return diffMinutes;

	}

	/**
	 * This method is a wrapper on DateFormatUtils.format() since it doesn't
	 * take care of null values.
	 * 
	 * @param date
	 *            date from database as a string
	 * @return formatted string for the ui
	 */
	public static final String format(Date date, String pattern) {

		String returnValue = (date == null) ? "" : DateFormatUtils.format(date,
				pattern);
		return returnValue;
	}

	/**
	 * This method to determine the number of days between two dates excluding
	 * Saturday and Sunday.
	 * 
	 * @param date
	 *            date to check
	 * @return boolean
	 */
	public static int getDateDifferenceInDaysExcludingWeekend(Date fromDate,
			Date toDate) {
		if (fromDate == null || toDate == null) {
			return 0;
		}

		if (fromDate.after(toDate)) {
			Date temp = fromDate;
			fromDate = toDate;
			toDate = temp;
		}

		Calendar calFrom = Calendar.getInstance();
		calFrom.setTime(fromDate);
		Calendar calTo = Calendar.getInstance();
		calTo.setTime(toDate);
		int iNoOfWorkingDays = 0;
		do {
			if (calFrom.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY
					&& calFrom.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
				iNoOfWorkingDays += 1;
			}
			calFrom.add(Calendar.DATE, 1);
		} while (calFrom.getTimeInMillis() < calTo.getTimeInMillis());
		return iNoOfWorkingDays;
	}

	/**
	 * This method generates a string representation of a date's date/time in
	 * the format you specify on input.
	 * 
	 * @param aMask
	 *            the date pattern the string is in
	 * @param aDate
	 *            a date object
	 * @return a formatted string representation of the date
	 * @see java.text.SimpleDateFormat
	 */
	public static final String getDateTime(String aMask, Date aDate) {
		SimpleDateFormat df = null;
		String returnValue = "";

		if (aDate == null) {
			logger.error("date is null!");
		} else {
			df = new SimpleDateFormat(aMask);
			returnValue = df.format(aDate);
		}

		return (returnValue);
	}

	/**
	 * Gets midnight of the day in which date lies
	 * 
	 * @param date
	 * @return
	 */
	public static Date getDateWithEndTimeOfTheDay(Date date) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);

		return cal.getTime();
	}

	/**
	 * Gets midnight of the day in which date lies
	 * 
	 * @param date
	 * @return
	 */
	public static Date getDateWithStartTimeOfTheDay(Date date) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);

		return cal.getTime();
	}

	/**
	 * Gets the first possible date of the month based on date passed
	 * 
	 * @param date
	 * @return
	 */
	public static Date getFirstDateOfMonth(Date date) {
		if (date != null) {
			Date tmpDate = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
			Calendar cal = Calendar.getInstance();
			cal.setTime(tmpDate);
			cal.set(Calendar.DAY_OF_MONTH, 1);

			return cal.getTime();
		}
		return null;

	}

	// /**
	// * This method returns the current date time in the format: MM/dd/yyyy
	// * HH:MM. a
	// *
	// * @param theTime
	// * the current time
	// * @return the current date/time
	// */
	// public static String getTimeNow(Date theTime) {
	// return getDateTime(timePattern, theTime);
	// }

	/**
	 * Gets the first possible date of the month based on the year and month
	 * passed
	 * 
	 * @param year
	 * @param month
	 * @return
	 */
	public static Date getFirstDateOfMonth(int year, int month) {
		Calendar date = new GregorianCalendar(year, month, 1);

		return date.getTime();
	}

	public static Date getFirstDateOfWeek(Date date) {
		if (date != null) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
			return cal.getTime();
		}
		return null;

	}

	/**
	 * Gets the first day of the current financial year.
	 * 
	 * @param addToCurrentYear
	 *            can be negative. Gets the first day of the current financial
	 *            year if zero is passed
	 * 
	 * @return the first day of the current financial year
	 */
	public static Date getFirstDayOfFinancialYear(int addToCurrentYear) {
		Calendar firstDayOfYear = Calendar.getInstance();

		firstDayOfYear.set(Calendar.DAY_OF_MONTH, 1);
		firstDayOfYear.set(Calendar.MONTH, Calendar.DECEMBER);
		firstDayOfYear.add(Calendar.YEAR, addToCurrentYear - 1);
		firstDayOfYear.set(Calendar.HOUR_OF_DAY, 0);
		firstDayOfYear.set(Calendar.MINUTE, 0);
		firstDayOfYear.set(Calendar.SECOND, 0);

		return firstDayOfYear.getTime();
	}

	/**
	 * Gets the first date of any year.
	 * 
	 * @param addToCurrentYear
	 *            can be negative. Gets the first day of the current year if
	 *            zero is passed
	 * 
	 * @return java.util.Date
	 */
	public static Date getFirstDayOfYear(int addToCurrentYear) {
		Calendar firstDayOfYear = Calendar.getInstance();

		firstDayOfYear.set(Calendar.DAY_OF_YEAR, 1);
		firstDayOfYear.add(Calendar.YEAR, addToCurrentYear);

		return firstDayOfYear.getTime();
	}

	/**
	 * Gets the first possible date of the month based on date passed
	 * 
	 * @param date
	 * @return
	 */
	public static Date getLastDateOfDay(Date date) {
		Date tmpDate = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
		Calendar cal = Calendar.getInstance();
		cal.setTime(tmpDate);
		cal.add(Calendar.DAY_OF_YEAR, 1);
		cal.add(Calendar.MILLISECOND, -1);

		return cal.getTime();
	}

	/**
	 * Gets the last possible date of the month based on date passed
	 * 
	 * @param date
	 * @return
	 */
	public static Date getLastDateOfMonth(Date date) {
		if (date != null) {
			Date tmpDate = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
			Calendar cal = Calendar.getInstance();
			cal.setTime(tmpDate);
			cal.set(Calendar.DAY_OF_MONTH, 1);
			cal.add(Calendar.MONTH, 1);
			cal.add(Calendar.MILLISECOND, -1);
			return cal.getTime();
		}
		return null;
	}

	/**
	 * Gets the last possible date of the month based on the year and month
	 * passed
	 * 
	 * @param year
	 * @param month
	 * @return
	 */
	public static Date getLastDateOfMonth(int year, int month) {
		Calendar date = new GregorianCalendar(year, month, 1);
		date.add(Calendar.MONTH, 1);
		date.add(Calendar.MILLISECOND, -1);

		return date.getTime();
	}

	public static Date getLastDateOfWeek(Date date) {
		if (date != null) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
			return cal.getTime();
		}
		return null;
	}

	/**
	 * Gets the last day of the current financial year.
	 * 
	 * @param addToCurrentYear
	 *            can be negative. Gets the last day of the current financial
	 *            year if zero is passed
	 * 
	 * @return the last day of the current financial year
	 */
	public static Date getLastDayOfFinancialYear(int addToCurrentYear) {
		Calendar lastDayOfYear = Calendar.getInstance();

		lastDayOfYear.set(Calendar.DAY_OF_MONTH, 30);
		lastDayOfYear.set(Calendar.MONTH, Calendar.NOVEMBER);
		lastDayOfYear.add(Calendar.YEAR, addToCurrentYear);
		lastDayOfYear.set(Calendar.HOUR_OF_DAY, 23);
		lastDayOfYear.set(Calendar.MINUTE, 23);
		lastDayOfYear.set(Calendar.SECOND, 59);

		return lastDayOfYear.getTime();
	}

	/**
	 * Gets the last date of any year.
	 * 
	 * @param addToCurrentYear
	 *            can be negative. Gets the last day of the current year if zero
	 *            is passed
	 * 
	 * @return java.util.Date
	 */
	public static Date getLastDayOfYear(int addToCurrentYear) {
		Calendar lastDayOfYear = Calendar.getInstance();

		lastDayOfYear.add(Calendar.YEAR, addToCurrentYear);
		lastDayOfYear.set(Calendar.MONTH, Calendar.DECEMBER);
		lastDayOfYear.set(Calendar.DAY_OF_MONTH, 31);

		return lastDayOfYear.getTime();
	}

	/**
	 * Returns the new date object the system is using.
	 * 
	 * @return Date
	 */
	public static Date getSystemDate() {
		return new Date();
	}

	/**
	 * This method returns the current date in the format: MM/dd/yyyy
	 * 
	 * @return the current date
	 * @throws ParseException
	 */
	public static Calendar getToday() throws ParseException {
		Date today = new Date();
		SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");

		// This seems like quite a hack (date -> string -> date),
		// but it works ;-)
		String todayAsString = df.format(today);
		Calendar cal = new GregorianCalendar();
		cal.setTime(convertStringToDate(todayAsString, "MM/dd/yyyy"));
		return cal;
	}

	/**
	 * Calculates the difference in hours of the given dates.
	 * 
	 * @param startDate
	 * @param endDate
	 * @return long
	 */
	public static long hourDifference(Date startDate, Date endDate) {

		GregorianCalendar startCalendar = new GregorianCalendar();
		startCalendar.setTime(startDate);

		GregorianCalendar endCalendar = new GregorianCalendar();
		endCalendar.setTime(endDate);
		long milliseconds1 = startCalendar.getTimeInMillis();
		long milliseconds2 = endCalendar.getTimeInMillis();
		long diff = milliseconds2 - milliseconds1;
		long diffHours = diff / (60 * 60 * 1000);

		return diffHours;
	}

	/**
	 * 
	 * @param date1
	 * @param date2
	 * @return
	 */
	public static boolean isSameDay(Date date1, Date date2) {
		if (date1 == null || date2 == null) {
			if (date1 == null && date2 == null) {
				return true;
			} else {
				return false;
			}
		} else {
			return DateUtils.isSameDay(date1, date2);
		}

	}

	/**
	 * This method to determine if the date is a weekend.
	 * 
	 * @param date
	 *            the date to check
	 * @return boolean
	 */
	public static boolean isWeekend(Date date) {
		Calendar cal = new GregorianCalendar();
		cal.setTime(date);

		if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY
				|| cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) {
			return true;
		}
		return false;
	}

	/**
	 * Calculates the difference in minutes of the given dates.
	 * 
	 * @param startDate
	 * @param endDate
	 * @return long
	 */
	public static long minuteDifference(Date startDate, Date endDate) {

		GregorianCalendar startCalendar = new GregorianCalendar();
		startCalendar.setTime(startDate);

		GregorianCalendar endCalendar = new GregorianCalendar();
		endCalendar.setTime(endDate);

		long milliseconds1 = startCalendar.getTimeInMillis();
		long milliseconds2 = endCalendar.getTimeInMillis();
		long diff = milliseconds2 - milliseconds1;

		long diffMinutes = diff / (60 * 1000);

		return diffMinutes;
	}

	/**
	 * Calculates the difference in weeks of the given dates.
	 * 
	 * @param startDate
	 * @param endDate
	 * @return int
	 */
	public static int weekDifference(Date startDate, Date endDate) {

		GregorianCalendar startCalendar = new GregorianCalendar();
		startCalendar.setTime(startDate);

		GregorianCalendar endCalendar = new GregorianCalendar();
		endCalendar.setTime(endDate);

		int weekCount = 0;
		while (startCalendar.equals(endCalendar)
				|| startCalendar.before(endCalendar)) {
			startCalendar.add(Calendar.WEEK_OF_YEAR, 1);
			weekCount++;
		}

		return weekCount;
	}

	private DateUtil() {
		super();
	}
}
